import stat from '../util/stat'
import fs from '../fs/index'
import path from '../../node/path'
import mkdirs from '../mkdirs'
import pathExists from '../path-exists'
import errorcode from '../../node/error-code'

export default function copySync (src, dest, opts?) {
    if (typeof opts === 'function') {
        opts = { filter: opts }
    }

    opts = opts || {}
    opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
    opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber


    const { srcStat, destStat } = stat.checkPathsSync(src, dest, 'copy', opts)
    stat.checkParentPathsSync(src, srcStat, dest, 'copy')
    if (opts.filter && !opts.filter(src, dest)) {
        return
    }
    const destParent = path.dirname(dest)
    if (!pathExists.pathExistsSync(destParent)) mkdirs.mkdirsSync(destParent)
    return getStats(destStat, src, dest, opts)
}

function startCopy (destStat, src, dest, opts) {
    if (opts.filter && !opts.filter(src, dest)) return;
    return getStats(destStat, src, dest, opts)
}

function getStats (destStat, src, dest, opts) {
    const statSync = opts.dereference ? fs.statSync : fs.lstatSync
    const srcStat = statSync(src)

    if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts)
    else if (srcStat.isFile() ||
    srcStat.isCharacterDevice() ||
    srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts)
    else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts)
    else if (srcStat.isSocket()) throw new Error(`Cannot copy a socket file: ${src}`)
    else if (srcStat.isFIFO()) throw new Error(`Cannot copy a FIFO pipe: ${src}`)
    throw new Error(`Unknown file: ${src}`)
}

function onFile (srcStat, destStat, src, dest, opts) {
    if (!destStat) return copyFile(srcStat, src, dest, opts);
    return mayCopyFile(srcStat, src, dest, opts);
}

function mayCopyFile (srcStat, src, dest, opts) {
    if (opts.overwrite) {
        fs.unlinkSync(dest)
        return copyFile(srcStat, src, dest, opts)
    } else if (opts.errorOnExist) {
        throw new Error(`'${dest}' already exists`)
    }
}

function copyFile (srcStat, src, dest, opts) {
    fs.copyFileSync(src, dest)
    return setDestMode(dest, srcStat.mode)
}

function fileIsNotWritable (srcMode) {
    return (srcMode & 0o200) === 0
}

function makeFileWritable (dest, srcMode) {
    return setDestMode(dest, srcMode | 0o200);
}

function setDestMode (dest, srcMode) {
    return fs.chmodSync(dest, srcMode);
}

function onDir (srcStat, destStat, src, dest, opts) {
    if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts);
    return copyDir(src, dest, opts);
}

function mkDirAndCopy (srcMode, src, dest, opts) {
    fs.mkdirSync(dest)
    copyDir(src, dest, opts)
    return setDestMode(dest, srcMode)
}

function copyDir (src, dest, opts) {
    fs.readdirSync(src).forEach(item => copyDirItem(item, src, dest, opts))
}

function copyDirItem (item, src, dest, opts) {
    const srcItem = path.join(src, item)
    const destItem = path.join(dest, item)
    const { destStat } = stat.checkPathsSync(srcItem, destItem, 'copy', opts)
    return startCopy(destStat, srcItem, destItem, opts)
}

function onLink(destStat, src, dest, opts) {
    let resolvedSrc = fs.readlinkSync(src)
    if (opts.dereference) {
        resolvedSrc = path.resolve('/', resolvedSrc)
    }

    if (!destStat) {
        return fs.symlinkSync(resolvedSrc, dest)
    } else {
        let resolvedDest
        try {
            resolvedDest = fs.readlinkSync(dest)
        } catch (err) {
            // dest exists and is a regular file or directory,
            // Windows may throw UNKNOWN error. If dest already exists,
            // fs throws error anyway, so no need to guard against it here.
            if (err.message === errorcode.errormessage.EINVAL) return fs.symlinkSync(resolvedSrc, dest)
            throw err
        }
        if (opts.dereference) {
            resolvedDest = path.resolve('/', resolvedDest)
        }
        if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) {
            throw new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`)
        }

        // prevent copy if src is a subdir of dest since unlinking
        // dest in this case would result in removing src contents
        // and therefore a broken symlink would be created.
        if (fs.statSync(dest).isDirectory() && stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
            throw new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`)
        }
        return copyLink(resolvedSrc, dest)
    }
}

function copyLink(resolvedSrc, dest) {
    fs.unlinkSync(dest)
    return fs.symlinkSync(resolvedSrc, dest)
}